/****************************************************************************************
**  File: ShaderManager.hpp
**  Author: Asteroth
**  Date: 18/05/2013
****************************************************************************************/

#ifndef __ZEN_SHADER_MANAGER_HPP__
#define __ZEN_SHADER_MANAGER_HPP__

#include <map>
#include <JSL.hpp>

#include "../sys/Resources.hpp"
#include "../debug.hpp"
#include "../gl.hpp"
#include "../math.hpp"

#include "Shader.hpp"
#include "ShaderProgram.hpp"

namespace zen {

	namespace video {

		class ShaderManager {

		private:

			std::map<std::string, ShaderProgram*> shaders;


		public:

			ShaderManager() { }

			ShaderManager(const std::string &config) {
				loadConfiguration(config);
			}

			inline ShaderProgram* get(const std::string &name) const {
				std::map<std::string, ShaderProgram*>::const_iterator iter = shaders.find(name);
				if (iter != shaders.end())
					return iter->second;
				return NULL;
			}

			inline void use(const std::string &name) const {
				std::map<std::string, ShaderProgram*>::const_iterator iter = shaders.find(name);
				if (iter != shaders.end())
					iter->second->use();
				else
					glUseProgram(0);
			}

			static inline void useDefault() {
				glUseProgram(0);
			}

			ShaderProgram* loadProgram(const std::string &name, const std::string &vertexShader, const std::string &geometryShader, const std::string &fragmentShader) {
				zen_call("ShaderManager::loadProgram(\"" + name + "\", \"" + vertexShader + "\", \"" + geometryShader + "\", \"" + fragmentShader + "\")");

				Shader* vsh = NULL;
				if (!vertexShader.empty()) {
					vsh = new Shader(Shader::VERTEX_SHADER, vertexShader);
					if (!vsh->isCompiled()) {
						zen_log("Error: Vertex shader \"" + vertexShader + "\" compile error:\n" + vsh->getInfoLog());
						delete vsh;
						return NULL;
					}
				}

				Shader* gsh = NULL;
				if (!geometryShader.empty()) {
					gsh = new Shader(Shader::GEOMETRY_SHADER, geometryShader);
					if (!gsh->isCompiled()) {
						zen_log("Error: Geometry shader \"" + geometryShader + "\" compile error:\n" + gsh->getInfoLog());
						delete gsh;
						delete vsh;
						return NULL;
					}
				}

				Shader* fsh = NULL;
				if (!fragmentShader.empty()) {
					fsh = new Shader(Shader::FRAGMENT_SHADER, fragmentShader);
					if (!fsh->isCompiled()) {
						zen_log("Error: Fragment shader \"" + fragmentShader + "\" compile error:\n" + fsh->getInfoLog());
						delete fsh;
						delete gsh;
						delete vsh;
						return NULL;
					}
				}

				ShaderProgram* program = new ShaderProgram();
				if (vsh != NULL)
					program->attachShader(vsh);
				if (gsh != NULL)
					program->attachShader(gsh);
				if (fsh != NULL)
					program->attachShader(fsh);

				program->link();
				delete vsh;
				delete gsh;
				delete fsh;
				if (program->isLinked()) {
					shaders.insert(std::pair<std::string, ShaderProgram*>(name, program));
					return program;
				} else {
					delete program;
					zen_log("Error: Could not link shader program \"" + name + "\".");
					return NULL;
				}
			}

			bool loadConfiguration(const std::string &config) {
				zen_call("ShaderManager::loadConfiguration(\"" + config + "\")");

				if (config.empty()) {
					zen_log("Error: Invalid configuration resource name.");
					return false;
				}

				jsl::Node* node = sys::Resources::load(config);
				if (node == NULL) {
					zen_log("Error: Failed to load the resource \"" + config + "\".");
					return false;
				}

				jsl::Node* directories = node->at("directories");
				for (size_t i = 0; i < directories->size(); i++)
					zglRegisterShaderSourceDirectory(directories->at(i)->asString());

				std::map<std::string, std::string> sources;
				jsl::Node* programs = node->at("programs");

				if (programs != NULL) {
					for (unsigned int i = 0; i < programs->size(); i++) {
						jsl::Node* child = programs->at(i);
						std::string name = child->at("name")->asString();

						jsl::Node* tmp = child->at("source");
						if (tmp != NULL) {
							sources.insert(std::pair<std::string, std::string>(name, tmp->value()));
							continue;
						}

						std::string vertex = child->at("vertex")->value();
						std::string geometry = child->at("geometry")->value();
						std::string fragment = child->at("fragment")->value();

						ShaderProgram* program = loadProgram(name, vertex, geometry, fragment);
						if (program == NULL) {
							zen_log("Error: Could not load shader program named \"" + name + "\".");
							continue;
						}

						jsl::Node* uniforms = child->at("uniforms");
						for (GLenum i = 0; i < ShaderUniforms::COUNT; i++) {
							std::string uniform = uniforms->at(ShaderUniforms::uniformNames[i])->value();
							if (!uniform.empty())
								program->uniforms.setUniform(i, uniform);
						}

						uniforms = child->at("inputUniforms");
						for (unsigned int j = 0; j < uniforms->size(); j++) {
							std::string uniform = uniforms->at(j)->value();
							if (!uniform.empty())
								program->uniforms.setUniform(ShaderUniforms::GENERIC_INPUT, uniform);
						}
					}

					glUseProgram(0);

					// linking shortcuts
					for (std::map<std::string, std::string>::iterator iter = sources.begin(); iter != sources.end(); iter++) {
						std::map<std::string, ShaderProgram*>::iterator shdr = shaders.find(iter->second);
						if (shdr != shaders.end())
							shaders[iter->first] = shdr->second;
						zen_else_log("Error: Invalid shader program source for shortcut \"" + iter->first + "\", shader program \"" + iter->second + "\" not found.");
					}
				} zen_else_log("Warning: Specifics seems to be empty or incorrect definition, no shader program loaded.");

				return true;
			}


		};

	} // namespace video

} // namespace zen

#endif // __ZEN_SHADER_MANAGER_HPP__
